/*
 * Copyright 2011-2025 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0.
 * See `LICENSE` in the project root for license information.
 */

package me.ijleex.platform.uploadfile.util;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileAttribute;
import java.util.Base64;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import me.ijleex.platform.framework.entity.Result;
import me.ijleex.platform.framework.util.RandomStringUtils;

/**
 * 上传文件到本地目录工具类
 *
 * @author liym
 * @since 2019-10-30 22:18 新建
 */
public abstract class LocalUploadFileUtil {

    private static final Logger logger = LoggerFactory.getLogger(LocalUploadFileUtil.class);

    /**
     * 本地文件目录最大深度（1-3之间，不要超过3）
     */
    private static final int MAX_DISK_FILE_DEPTH = 2;
    /**
     * 数字开头目录（1-{@value}），实际为 0-{@value}
     */
    private static final int DIGITAL_DIR_COUNT = 1;

    private LocalUploadFileUtil() {
    }

    /**
     * 创建上传文件的目录结构
     *
     * <p>在 {@code localPath} 目录下，创建一系列的目录；创建规则为：用 0-{@value DIGITAL_DIR_COUNT} 的数字开头，a-z 的字母结尾，
     * 拼接成目录名，循环创建目录，如 0a、0b、1a、1b……等，创建目录的深度为 {@value MAX_DISK_FILE_DEPTH}。</p>
     *
     * @param localPath 本地目录，如 D:/x-files 或 /opt/apps/upload-file
     */
    public static void initLocalFsDir(String localPath) {
        if (StringUtils.isBlank(localPath)) {
            logger.error("initLocalFsDir: localPath=null");
            return;
        }
        Path startPath = Paths.get(localPath);

        // 采样 10 次，如果目录都存在，则视为本地目录已经初始化
        boolean exists = true;
        for (int i = 0; i < 10; i++) {
            Path testPath = startPath.resolve(getRandomPath());
            if (!Files.exists(testPath, LinkOption.NOFOLLOW_LINKS)) {
                exists = false;
                break;
            }
        }
        if (exists) {
            return;
        }

        try {
            if (!Files.exists(startPath)) {
                Files.createDirectories(startPath);
            }
            if (!Files.isDirectory(startPath) || !Files.isWritable(startPath)) {
                logger.error("上传文件的根目录不可写或不是目录，文件目录初始化失败：{}", startPath);
                System.exit(-1);
            }
        } catch (Exception ex) {
            logger.error("创建上传文件目录失败：{}", localPath, ex);
            System.exit(-1);
        }

        // 提交创建文件夹任务到线程池 2019-08-08 14:49:43
        // 创建有名称的线程
        final ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("local-fs-mkdir-executor-%d")
                .setDaemon(true)
                .build();
        // 定长线程池
        final ExecutorService executorService = new ThreadPoolExecutor(1, 2,
                0L, TimeUnit.NANOSECONDS, // 空闲线程的存活时长
                new LinkedBlockingQueue<>(2),
                namedThreadFactory,
                // 线程池的饱和策略，当提交任务数超过 maximumPoolSize+workQueue 之和时，如果继续提交任务，使用该策略处理任务
                new ThreadPoolExecutor.CallerRunsPolicy());
        executorService.execute(() -> {
            try {
                createDirectories(localPath, 0, MAX_DISK_FILE_DEPTH);
            } catch (Exception e) {
                logger.error("递归创建上传目录失败", e);
            }
        });
        executorService.shutdown();
    }

    /**
     * 生成随机目录（片断）
     *
     * @return Path，如 {@code /0a/0a}
     */
    public static String getRandomPath() {
        StringBuilder partPath = new StringBuilder(6);
        for (int i = 0; i < MAX_DISK_FILE_DEPTH; i++) {
            partPath.append('/');
            partPath.append(RandomUtils.secure().randomInt(0, DIGITAL_DIR_COUNT + 1));
            partPath.append(randomLetter());
        }
        return partPath.toString();
    }

    /**
     * 随机获取一个字母
     *
     * @return a-z 的随机字符
     * @since 2019-08-08 09:02
     */
    private static char randomLetter() {
        return (char) (Math.random() * 26 + 97);
    }

    /**
     * 递归创建本地目录
     *
     * <p>每级目录的名称为数字+字母，数字部分的个数为 0-{@value #DIGITAL_DIR_COUNT}，字母为 a-z，循环创建，直到深度达到 {@code maxDepth}。</p>
     *
     * @param parentPath 父目录
     * @param depth 当前深度
     * @param maxDepth 最大深度
     * @throws Exception {@link Files#createDirectories(Path, FileAttribute[])} 可能发生的异常
     */
    private static void createDirectories(String parentPath, int depth, int maxDepth) throws Exception {
        System.out.println("parentPath=" + parentPath + ", depth=" + depth + ", maxDepth=" + maxDepth);
        if (depth >= maxDepth) {
            return;
        }
        for (int i = 0; i <= DIGITAL_DIR_COUNT; i++) {
            for (char c = 'a'; c <= 'z'; c++) {
                Path path = Paths.get(parentPath, Integer.toString(i) + c);
                Files.createDirectories(path);
                createDirectories(path.toString(), depth + 1, maxDepth);
            }
        }
    }

    /**
     * 将文件写入本地文件系统
     *
     * @param localPath 本地上传文件目录的根目录，如 D:/x-files 或 /opt/apps/upload-file
     * @param fileBytes 文件内容
     * @param originalFilename 原始文件名称
     * @param fileType 文件类型，如 image/png、text/plain 等
     * @param fileSize 文件大小（Bytes）
     * @param encrypt 是否加密文件
     * @return true/false
     * @see #getFile(String, String, String, String, long, boolean)
     * @since 2019-10-30 22:29
     * @since 2019-12-06 16:09 添加参数：fileType、fileSize、encrypt，支持加密保存文件
     */
    public static Result putFile(String localPath, byte[] fileBytes, String originalFilename, String fileType, long fileSize, boolean encrypt) {
        // TODO 检查本地目录是否已经创建
        if (fileBytes == null || fileBytes.length == 0) {
            logger.error("上传文件失败：文件内容为空");
            return Result.error("文件内容为空");
        }

        String partPath = getRandomPath();
        String partName = RandomStringUtils.generate(25);
        String extName = getFileExtension(originalFilename);
        String fileName = partName + extName;
        try {
            if (encrypt) {
                // 加密文件内容
                fileBytes = FileCryptoUtil.encrypt(fileBytes, originalFilename, fileType, fileSize);

                // 加密文件，在文件名称后面添加一个感叹号（!）
                fileName = fileName + '!';
            }

            Path localFile = Paths.get(localPath, partPath, fileName);
            Files.write(localFile, fileBytes);
            logger.debug("保存文件成功：[{}]", localFile);

            return Result.ok("上传文件成功").addData("path", partPath + '/' + fileName);
        } catch (IOException e) {
            logger.error("putFile error", e);
            return Result.error("上传文件失败：" + e.getMessage());
        }
    }

    /**
     * 获取文件扩展名（Filename Extension，或后缀名）
     *
     * <p>如果文件名以 ‘.' 为结尾，将返回 ‘.'；如果无扩展名且不以 ‘.' 结尾，将返回空值。</p>
     *
     * <pre>
     * getFileExtension("index.html");  -> return ".html"
     * getFileExtension("index.");      -> return "."
     * getFileExtension("index");       -> return ""
     * </pre>
     *
     * @param fileName 文件名
     * @return 文件后缀名，如 {@code XXX.docx}，返回 {@code ".docx"}
     */
    private static String getFileExtension(String fileName) {
        int dot = fileName.lastIndexOf(".");
        return dot == -1 ? "" : fileName.substring(dot);
    }

    /**
     * 根据已配置的本地文件的 URL 前缀与本地文件路径，构建请求地址
     *
     * @param pathPattern 请求本地文件的 URL 前缀，如 /local-fs/**
     * @param path 路径
     * @since 2019-07-22 14:24
     */
    public static String buildFileUrl(String pathPattern, String path) {
        int idx = pathPattern.lastIndexOf('/');
        String prefix = pathPattern.substring(0, idx);
        return prefix + path;
    }

    /**
     * 获取文件
     *
     * @param localPath 本地上传文件目录的根目录，如 D:/x-files 或 /opt/apps/upload-file
     * @param partPath 本地文件路径，如 /6g/9d/rYarzNMLZYjxxRpwWypzVNbvJ.jpg
     * @param originalFilename 原始文件名称，非加密文件可为空
     * @param contentType 文件类型，如 image/png、text/plain 等，非加密文件可为空
     * @param fileSize 文件大小（Bytes），非加密文件可为空
     * @param encrypt 是否加密文件
     * @return 结果：文件内容，以 Base64 编码，如 Png 图片，返回 "data:image/png;base64,..."
     * @see #putFile(String, byte[], String, String, long, boolean)
     * @see Files#readAllBytes(Path)
     * @since 2019-12-24 23:35
     */
    public static Result getFile(String localPath, String partPath, String originalFilename, String contentType, long fileSize, boolean encrypt) {
        Path localFile = Paths.get(localPath, partPath);

        // 检查文件是否存在，不包含符号链接文件
        if (!Files.exists(localFile, LinkOption.NOFOLLOW_LINKS)) {
            return Result.error("本地文件不存在：" + partPath);
        }

        try {
            byte[] fileBytes = Files.readAllBytes(localFile);
            if (fileBytes.length == 0) {
                return Result.error("文件内容为空：" + partPath);
            }
            if (encrypt) {
                fileBytes = FileCryptoUtil.decrypt(fileBytes, originalFilename, contentType, fileSize);
                if (fileBytes.length == 0) {
                    return Result.error("获取文件失败：" + partPath);
                }
            }

            String fileBase64 = Base64.getEncoder().encodeToString(fileBytes);

            // 返回 "" 格式的文件内容
            return Result.ok("data:" + contentType + ";base64," + fileBase64);
        } catch (IOException e) {
            logger.error("getFile error", e);
            return Result.error("获取文件失败：" + partPath);
        }
    }

}
